In [188]:
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from scipy.stats import chi2_contingency
import plotly.express as px
import plotly.figure_factory as ff
from PIL import Image
import pynarrative as pn
import altair as alt
import streamlit as st

df = pd.read_csv("data_toxins__structures_merge_drop.csv")

print(df.shape)
(3678, 57)
In [189]:
print(df.columns)
Index(['id', 'common_name', 'description', 'cas', 'chemical_formula',
       'appearance', 'route_of_exposure', 'mechanism_of_toxicity',
       'metabolism', 'toxicity', 'lethaldose', 'carcinogenicity', 'use_source',
       'health_effects', 'symptoms', 'export', 'moldb_smiles', 'moldb_formula',
       'moldb_inchi', 'moldb_inchikey', 'moldb_average_mass', 'origin',
       'state', 'carcinogenicity_grouped', 'carcinogenicity_label',
       'types_all', 'locations_all', 'DATABASE_ID', 'DATABASE_NAME', 'SMILES',
       'INCHI_IDENTIFIER', 'INCHI_KEY', 'FORMULA', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_IUPAC', 'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE',
       'JCHEM_NUMBER_OF_RINGS', 'JCHEM_PHYSIOLOGICAL_CHARGE',
       'JCHEM_POLAR_SURFACE_AREA', 'JCHEM_REFRACTIVITY',
       'JCHEM_ROTATABLE_BOND_COUNT', 'JCHEM_RULE_OF_FIVE',
       'JCHEM_TRADITIONAL_IUPAC', 'JCHEM_VEBER_RULE', 'NAME', 'CAS',
       'SYNONYMS', 'TYPES', 'ID', 'smiles', 'ALOGPS_SOLUBILITY'],
      dtype='object')

Basandomi sul gruppo IARC di riferimento, per una migliore interpretazione dei dati aggrego e trasformo le precedenti classi di cancerogenicità:

0 = non classificato o non cancerogeno (Gruppo 3);

1 = cancerogeno certo (Gruppo 1) o possibile/probabile cancerogeno (Gruppi 2A/2B);

In [190]:
def convert_carcinogenicity(text):
    text = str(text).lower()

    if "group 1" in text or "1, carcinogenic to humans" in text:
        return 1
    elif "group 2a" in text or "2a, probably carcinogenic to humans" in text:
        return 1
    elif "group 2b" in text or "2b, possibly carcinogenic to humans" in text:
        return 1
    elif "group 3" in text or "3, not classifiable" in text:
        return 0
    elif "no indication" in text or "not listed by iarc" in text:
        return 0
    else:
        return 0  # default per altri casi non classificati

# Applica la funzione al DataFrame
df['carcinogenicity_score'] = df['carcinogenicity'].apply(convert_carcinogenicity)
In [191]:
unique_values3 = df["carcinogenicity_score"].unique()
print(unique_values3)
[1 0]
In [192]:
print(df.isna().sum())
id                                 0
common_name                        0
description                        0
cas                               50
chemical_formula                 136
appearance                       206
route_of_exposure                730
mechanism_of_toxicity            434
metabolism                       779
toxicity                        2510
lethaldose                      3267
carcinogenicity                    4
use_source                       662
health_effects                   780
symptoms                         814
export                             0
moldb_smiles                     135
moldb_formula                    132
moldb_inchi                      132
moldb_inchikey                   132
moldb_average_mass               132
origin                            10
state                             21
carcinogenicity_grouped            4
carcinogenicity_label              4
types_all                          4
locations_all                    163
DATABASE_ID                      159
DATABASE_NAME                    159
SMILES                           160
INCHI_IDENTIFIER                 159
INCHI_KEY                        159
FORMULA                          159
JCHEM_ACCEPTOR_COUNT             162
JCHEM_AVERAGE_POLARIZABILITY     162
JCHEM_BIOAVAILABILITY            164
JCHEM_DONOR_COUNT                162
JCHEM_FORMAL_CHARGE              159
JCHEM_GHOSE_FILTER               160
JCHEM_IUPAC                      166
JCHEM_LOGP                       169
JCHEM_MDDR_LIKE_RULE             164
JCHEM_NUMBER_OF_RINGS            164
JCHEM_PHYSIOLOGICAL_CHARGE       163
JCHEM_POLAR_SURFACE_AREA         162
JCHEM_REFRACTIVITY               164
JCHEM_ROTATABLE_BOND_COUNT       164
JCHEM_RULE_OF_FIVE               161
JCHEM_TRADITIONAL_IUPAC          166
JCHEM_VEBER_RULE                 164
NAME                             159
CAS                              168
SYNONYMS                         242
TYPES                            163
ID                               159
smiles                           159
ALOGPS_SOLUBILITY                794
carcinogenicity_score              0
dtype: int64
In [193]:
# Elimino variabili non informative o con troppi valori NA
df_clean = df.drop(columns=["toxicity", "lethaldose", "export", "DATABASE_ID", "ID", "DATABASE_NAME",
                            "ALOGPS_SOLUBILITY", "carcinogenicity_label", "TYPES"])
In [194]:
# Elimino i valori NA nelle variabili rimaste dopo la pulizia
df_clean = df_clean.dropna()
In [195]:
print(df_clean.shape)
(1790, 49)
In [196]:
df_clean.to_csv('df_clean.csv', index=False)
In [197]:
# Plot della distribuzione usando seaborn
plt.figure(figsize=(8, 6))
sns.countplot(x='carcinogenicity_score', data=df_clean, palette='pastel')
plt.xticks([0, 1], ['0 (non classificati)', '1 (cancerogeni o possibili cancerogeni)'])
plt.xlabel('Classe di cancerogenicità')
plt.ylabel('Count')
plt.title('Distribuzione delle classi di cancerogenicità')
plt.show()

non_classificato = df_clean[df_clean["carcinogenicity_score"] == 0]
cancerogeno = df_clean[df_clean["carcinogenicity_score"] == 1]
print(f"Numero di non classificati: {len(non_classificato)}")
print(f"Numero di cancerogeni / possibili cancerogeni: {len(cancerogeno)}")
C:\Users\Maura\AppData\Local\Temp\ipykernel_9816\1491774361.py:3: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
Numero di non classificati: 1147
Numero di cancerogeni / possibili cancerogeni: 643
In [198]:
df_numeriche = df_clean.select_dtypes(include=['number'])
print(df_numeriche.columns)
Index(['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE', 'JCHEM_NUMBER_OF_RINGS',
       'JCHEM_PHYSIOLOGICAL_CHARGE', 'JCHEM_POLAR_SURFACE_AREA',
       'JCHEM_REFRACTIVITY', 'JCHEM_ROTATABLE_BOND_COUNT',
       'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE', 'carcinogenicity_score'],
      dtype='object')

Correlazione tra ogni variabile numerica e la variabile target

In [199]:
from mlxtend.plotting import scatterplotmatrix
colss = ['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT',
       'JCHEM_AVERAGE_POLARIZABILITY', 'JCHEM_BIOAVAILABILITY',
       'JCHEM_DONOR_COUNT', 'JCHEM_FORMAL_CHARGE', 'JCHEM_GHOSE_FILTER',
       'JCHEM_LOGP', 'JCHEM_MDDR_LIKE_RULE', 'JCHEM_NUMBER_OF_RINGS',
       'JCHEM_PHYSIOLOGICAL_CHARGE', 'JCHEM_POLAR_SURFACE_AREA',
       'JCHEM_REFRACTIVITY', 'JCHEM_ROTATABLE_BOND_COUNT',
       'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE', 'carcinogenicity_score']

from mlxtend.plotting import heatmap    
cm = np.corrcoef(df_clean[colss].values.T)   # Calcolo coefficiente di correlazione tra colonne  # Converto Dataframe in array Numpy   # Traspongo array
plt.figure(figsize=(20, 20))
hm = heatmap(cm,                          # Matrice di correlazione come input
             row_names=colss,              # Imposto le etichette per le righe
             column_names=colss,
            cell_font_size=7)           # Imposto le etichette per le colonne

plt.xticks(fontsize=7, rotation=90)
plt.yticks(fontsize=7, rotation=0)
plt.show()
<Figure size 2000x2000 with 0 Axes>
No description has been provided for this image
In [200]:
df_clean.describe()
Out[200]:
moldb_average_mass JCHEM_ACCEPTOR_COUNT JCHEM_AVERAGE_POLARIZABILITY JCHEM_BIOAVAILABILITY JCHEM_DONOR_COUNT JCHEM_FORMAL_CHARGE JCHEM_GHOSE_FILTER JCHEM_LOGP JCHEM_MDDR_LIKE_RULE JCHEM_NUMBER_OF_RINGS JCHEM_PHYSIOLOGICAL_CHARGE JCHEM_POLAR_SURFACE_AREA JCHEM_REFRACTIVITY JCHEM_ROTATABLE_BOND_COUNT JCHEM_RULE_OF_FIVE JCHEM_VEBER_RULE carcinogenicity_score
count 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000 1790.000000
mean 297.962796 1.615084 23.508142 0.916760 0.543575 0.034637 0.339665 3.095456 0.040782 1.644693 -0.030726 33.713061 59.623390 2.037989 0.651397 0.660894 0.359218
std 171.065752 2.543066 16.139935 0.276322 1.399932 0.614640 0.473728 3.170604 0.197840 1.650588 0.680031 50.771995 42.072629 3.622134 0.476661 0.473538 0.479905
min 9.011100 0.000000 0.435901 0.000000 0.000000 -12.000000 0.000000 -14.529231 0.000000 0.000000 -5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
25% 193.242300 0.000000 10.749738 1.000000 0.000000 0.000000 0.000000 0.580984 0.000000 0.000000 0.000000 0.000000 26.205600 0.000000 0.000000 0.000000 0.000000
50% 283.230350 0.000000 24.645218 1.000000 0.000000 0.000000 0.000000 3.010901 0.000000 2.000000 0.000000 17.070000 65.581200 1.000000 1.000000 1.000000 0.000000
75% 360.878000 3.000000 31.397646 1.000000 1.000000 0.000000 1.000000 5.567426 0.000000 3.000000 0.000000 52.600000 80.055600 3.000000 1.000000 1.000000 1.000000
max 1961.036000 28.000000 156.669938 1.000000 20.000000 10.000000 1.000000 18.436200 1.000000 18.000000 6.000000 627.070000 397.851700 48.000000 1.000000 1.000000 1.000000
In [201]:
print(dir(pn))
['Story', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'story']
In [202]:
# Calcoli riassuntivi
mean_mass = df_clean["moldb_average_mass"].mean()
mean_logp = df_clean["JCHEM_LOGP"].mean()
mean_refractivity = df_clean["JCHEM_REFRACTIVITY"].mean()
perc_exogenous = df_clean[df_clean["origin"] == "Exogenous"].shape[0] / df_clean.shape[0] * 100
perc_carc = df_clean[df_clean["carcinogenicity_score"] == 1].shape[0] / df_clean.shape[0] * 100

# Narrazione testuale
story_text = f"""
L'analisi del dataset ha rivelato alcune caratteristiche chiave delle molecole in relazione alla loro potenziale cancerogenicità.

📊 Complessivamente, il valore medio della massa molecolare è di circa {mean_mass:.1f} g/mol, indicando la presenza di molecole di dimensioni medio-alte nel dataset. Il valore medio di LogP è pari a {mean_logp:.2f}, suggerendo un grado di lipofilia moderato: molte molecole hanno quindi la potenzialità di attraversare facilmente le membrane cellulari. La rifrazione molare media, un indice della polarizzabilità elettronica, è {mean_refractivity:.2f}, coerente con la presenza di molecole complesse.

🧪 Dal punto di vista tossicologico, il dataset mostra che circa il {perc_carc:.1f}% delle molecole sono classificate come cancerogene o potenzialmente tali. Inoltre, il {perc_exogenous:.1f}% delle molecole ha origine esogena, sottolineando come molte di esse possano derivare da sostanze industriali, contaminanti ambientali o farmaci.

📈 Un'osservazione interessante riguarda le relazioni tra le variabili: molecole con massa molecolare elevata tendono ad avere anche valori alti di rifrazione, il che è chimicamente plausibile poiché una maggiore massa comporta una struttura più complessa e quindi più facilmente polarizzabile.

🧬 Le molecole cancerogene tendono a concentrarsi nella zona del grafico che mostra alti valori sia di massa che di rifrazione molare. Questo suggerisce che molecole più pesanti e polarizzabili possano avere una maggiore capacità di interazione con bersagli biologici, come DNA o proteine cellulari, aumentando il rischio di effetti mutageni.

💡 Tuttavia, è importante sottolineare che nessuna singola variabile è risultata fortemente predittiva della cancerogenicità. Piuttosto, il rischio sembra emergere da un insieme di caratteristiche strutturali, confermando la necessità di un approccio multivariato per una valutazione più accurata.

L'esplorazione dei dati, dunque, mostra che la cancerogenicità non dipende da un solo fattore, ma è il risultato di **interazioni sinergiche tra massa, polarità, stato fisico, lipofilia e origine molecolare**.
"""

# Storia
story = pn.Story(story_text)
print(story_text)
L'analisi del dataset ha rivelato alcune caratteristiche chiave delle molecole in relazione alla loro potenziale cancerogenicità.

📊 Complessivamente, il valore medio della massa molecolare è di circa 298.0 g/mol, indicando la presenza di molecole di dimensioni medio-alte nel dataset. Il valore medio di LogP è pari a 3.10, suggerendo un grado di lipofilia moderato: molte molecole hanno quindi la potenzialità di attraversare facilmente le membrane cellulari. La rifrazione molare media, un indice della polarizzabilità elettronica, è 59.62, coerente con la presenza di molecole complesse.

🧪 Dal punto di vista tossicologico, il dataset mostra che circa il 35.9% delle molecole sono classificate come cancerogene o potenzialmente tali. Inoltre, il 95.9% delle molecole ha origine esogena, sottolineando come molte di esse possano derivare da sostanze industriali, contaminanti ambientali o farmaci.

📈 Un'osservazione interessante riguarda le relazioni tra le variabili: molecole con massa molecolare elevata tendono ad avere anche valori alti di rifrazione, il che è chimicamente plausibile poiché una maggiore massa comporta una struttura più complessa e quindi più facilmente polarizzabile.

🧬 Le molecole cancerogene tendono a concentrarsi nella zona del grafico che mostra alti valori sia di massa che di rifrazione molare. Questo suggerisce che molecole più pesanti e polarizzabili possano avere una maggiore capacità di interazione con bersagli biologici, come DNA o proteine cellulari, aumentando il rischio di effetti mutageni.

💡 Tuttavia, è importante sottolineare che nessuna singola variabile è risultata fortemente predittiva della cancerogenicità. Piuttosto, il rischio sembra emergere da un insieme di caratteristiche strutturali, confermando la necessità di un approccio multivariato per una valutazione più accurata.

L'esplorazione dei dati, dunque, mostra che la cancerogenicità non dipende da un solo fattore, ma è il risultato di **interazioni sinergiche tra massa, polarità, stato fisico, lipofilia e origine molecolare**.

In [203]:
# Seleziono le colonne numeriche, escludendo target
df_numeriche = df_clean.select_dtypes(include=[np.number]).drop(columns='carcinogenicity_score')

# Calcolo la correlazione con il target
corr = df_clean[df_numeriche.columns].corrwith(df_clean['carcinogenicity_score'])

print(corr)

# Variabili con correlazione significativa (es. > 0.1)
selezionate = corr[abs(corr) > 0.1].index.tolist()
print("Variabili utili:", selezionate)
moldb_average_mass              0.114990
JCHEM_ACCEPTOR_COUNT           -0.185265
JCHEM_AVERAGE_POLARIZABILITY   -0.058625
JCHEM_BIOAVAILABILITY          -0.107389
JCHEM_DONOR_COUNT              -0.136881
JCHEM_FORMAL_CHARGE             0.001381
JCHEM_GHOSE_FILTER             -0.308332
JCHEM_LOGP                      0.180770
JCHEM_MDDR_LIKE_RULE           -0.083735
JCHEM_NUMBER_OF_RINGS          -0.103406
JCHEM_PHYSIOLOGICAL_CHARGE     -0.048374
JCHEM_POLAR_SURFACE_AREA       -0.189558
JCHEM_REFRACTIVITY             -0.058404
JCHEM_ROTATABLE_BOND_COUNT     -0.189539
JCHEM_RULE_OF_FIVE             -0.273308
JCHEM_VEBER_RULE                0.191967
dtype: float64
Variabili utili: ['moldb_average_mass', 'JCHEM_ACCEPTOR_COUNT', 'JCHEM_BIOAVAILABILITY', 'JCHEM_DONOR_COUNT', 'JCHEM_GHOSE_FILTER', 'JCHEM_LOGP', 'JCHEM_NUMBER_OF_RINGS', 'JCHEM_POLAR_SURFACE_AREA', 'JCHEM_ROTATABLE_BOND_COUNT', 'JCHEM_RULE_OF_FIVE', 'JCHEM_VEBER_RULE']
In [204]:
fig = px.scatter(df_clean, x='moldb_average_mass', y='carcinogenicity_score',
                 color='origin',  # oppure altra categorica
                 size='JCHEM_ACCEPTOR_COUNT',  # opzionale
                 hover_data=['JCHEM_LOGP', 'JCHEM_DONOR_COUNT'],
                 title='Relazione tra peso molecolare e carcinogenicità')
fig.show()


top_corr = corr[abs(corr) > 0.1]

# Converto in DataFrame
df_top_corr = top_corr.reset_index()

# Rinomino le due colonne
df_top_corr = df_top_corr.iloc[:, :2]
df_top_corr.columns = ['Variabile', 'Correlazione']

# Barplot con Plotly
fig = px.bar(df_top_corr, x='Variabile', y='Correlazione',
             title='Correlazione con carcinogenicity_score',
             labels={'Variabile': 'Variabile', 'Correlazione': 'Correlazione'})
fig.update_layout(xaxis_tickangle=45)
fig.show()


fig = px.scatter(df_clean, x='moldb_average_mass', y='carcinogenicity_score',
                 color='state',  # oppure altra categorica
                 size='JCHEM_POLAR_SURFACE_AREA',  # opzionale
                 hover_data=['JCHEM_LOGP', 'JCHEM_POLAR_SURFACE_AREA'],
                 title='Relazione tra peso molecolare e carcinogenicità')
fig.show()
In [205]:
# Impostazioni grafiche
sns.set(style="whitegrid", palette="muted", font_scale=1.2)

# Variabili molecolari da analizzare
vars_molecolari = ['moldb_average_mass', 'JCHEM_LOGP', 'state', 'origin']
In [206]:
# 1️⃣ Istogrammi / Density plot per ogni variabile molecolare
for col in vars_molecolari:
    plt.figure(figsize=(8,4))
    sns.histplot(data=df_clean, x=col, kde=True, hue='carcinogenicity_score', multiple="stack", palette='deep')
    plt.title(f'Distribuzione di {col} per classe di cancerogenicità')
    plt.show()

# ----------------------------------------------
# 2️⃣ Boxplot
for col in vars_molecolari:
    plt.figure(figsize=(8,4))
    sns.boxplot(data=df_clean, x='carcinogenicity_score', y=col, palette='deep')
    plt.title(f'{col} per classe di cancerogenicità')
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_9816\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_9816\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_9816\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
C:\Users\Maura\AppData\Local\Temp\ipykernel_9816\458371011.py:12: FutureWarning:



Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `y` variable to `hue` and set `legend=False` for the same effect.


No description has been provided for this image
In [207]:
fig_box = px.scatter(df_clean, x='moldb_average_mass', y='JCHEM_REFRACTIVITY',
                 color='carcinogenicity_score',
                 hover_name='common_name',
                 hover_data=['state', 'origin', 'JCHEM_LOGP'])
plt.show()
In [208]:
# Pairplot per esplorare relazioni tra tutte le variabili molecolari
sns.pairplot(df_clean, vars=df_numeriche, hue='carcinogenicity_score', palette='deep')
plt.suptitle('Relazioni tra variabili molecolari e cancerogenicità', y=1.02)
plt.show()
No description has been provided for this image